/**
 * 导出测试用例（YML格式，并打包）
 *
 * Created by qinmengchao on 2020/09/28.
 */

const dot = require('dot');
dot.templateSettings.strip = false;
const moment = require('moment');
const path = require('path');
const fs = require('fs-extra');
const logger = require('../logger');
const files = require('./../files');
const tests = require('./../tests');
const utility = require('./../utility');
const functions = require('./../functions');
const FunctionVariable = require('./../mongo/function-variable');
const TestCase = require('./../mongo/test-case');
const YMLTestCaseTransformer = require('./yml-transformer');
const YML_VERSION = '4.0.18';

/**
 * 收集变量信息
 * @param area {String} 变量所在数据域
 * @param scope {String} 变量输入输出类型
 * @param variable {Object} 变量
 * @param testCase {Object} 测试用例
 * @param values {String} 变量值
 * @param stubsUsed {Object} 使用过的stub
 * @param index {Object} 测试用例索引
 * @param path {String} 路径
 */
function collectVariableName(area, scope, variable, testCase, values, stubsUsed, index, path) {
  let currentPath;
  if (variable.kind === testCase.VARIABLE_KIND.return) {
    currentPath = path;
  } else {
    currentPath = path === '' ? variable.name : [path, variable.name].join('.');
  }
  let findValue = false;
  switch (variable.type) {
    case testCase.VARIABLE_TYPE.pointer:
      findValue = true;
      break;
    case testCase.VARIABLE_TYPE.basic:
      findValue = true;
      break;
    case testCase.VARIABLE_TYPE.functionPointer:
      if (variable.data['@value']) {
        stubsUsed.add(variable.data['@value']);
      }
      findValue = true;
      break;
    case testCase.VARIABLE_TYPE.record:
      variable.children.forEach((child) => {
        collectVariableName(area, scope, child, testCase, values, stubsUsed, index, currentPath);
      });
      break;
    case testCase.VARIABLE_TYPE.array:
      variable.children.forEach((child) => {
        collectVariableName(area, scope, child, testCase, values, stubsUsed, index, path);
      });
      break;
    default:
      break;
  }
  if (findValue) {
    const variables = values[scope][area];
    if (!variables[currentPath]) {
      variables[currentPath] = { data: [], count: 0 };
    }
    while (variables[currentPath].count !== index) {
      variables[currentPath].count += 1;
      variables[currentPath].data.push('*none*');
    }
    variables[currentPath].count += 1;
    if (variable.type === testCase.VARIABLE_TYPE.functionPointer && variable.data['@value']) {
      variables[currentPath].data.push(variable.data['@value'] + testCase.id);
    } else if (variable.type === testCase.VARIABLE_TYPE.pointer && variable.data['@value']) {
      if (variable.data['@value'] === '0') {
        variables[currentPath].data.push(variable.data['@value']);
      } else {
        variables[currentPath].data.push(variable.data['@value'].split('&')[1]);
      }
    } else {
      variables[currentPath].data.push(variable.data['@value'] || '*none*');
    }
  }
}

/**
 * 收集桩函数数据
 * @param variable {Object} 变量
 * @param result {Object} 存放收集数据
 * @param path {String} 路径
 * @param currentPath {String} 当前路径
 * @param func {Function} 函数，收集桩函数某个域数据
 */
function collectStubData(variable, result, path, currentPath, func, testCase) {
  switch (variable.type) {
    case testCase.VARIABLE_TYPE.record:
      variable.children.forEach((child) => {
        func(child, result, testCase, currentPath);
      });
      break;
    case testCase.VARIABLE_TYPE.array:
      variable.children.forEach((child) => {
        func(child, result, testCase, path);
      });
      break;
    case testCase.VARIABLE_TYPE.pointer:
      if (variable.data && variable.data['@value']) {
        if (variable.data['@value'] !== '0') {
          const [, value] = variable.data['@value'].split('@');
          result[currentPath] = value;
        } else {
          result[currentPath] = variable.data['@value'];
        }
      }
      break;
    case testCase.VARIABLE_TYPE.basic:
      if (variable.data && variable.data['@value']) {
        result[currentPath] = variable.data['@value'];
      }
      break;
    default:
      break;
  }
}

/**
 * 收集桩函数参数数值
 * @param variable {Object} 变量
 * @param result {Object} 存放收集数据
 * @param path {String} 变量路径
 */
function collectStubParams(variable, result, testCase, path = '') {
  const currentPath = path === '' ? variable.name : [path, variable.name].join('.');
  collectStubData(variable, result, path, currentPath, collectStubParams, testCase);
}

/**
 * 收集桩函数返回值数值
 * @param variable {Object} 变量
 * @param result {Object} 存放收集数据
 * @param path {String} 变量路径
 */
function collectStubReturn(variable, result, testCase, path = '') {
  let currentPath;
  if (variable.kind === testCase.VARIABLE_KIND.stubReturn) {
    currentPath = '_ret';
  } else {
    currentPath = [path, variable.name].join('.');
  }
  collectStubData(variable, result, path, currentPath, collectStubReturn, testCase);
}

/**
 * 收集桩函数指针目标的数值
 * @param variable {Object} 变量
 * @param result {Object} 存放收集数据
 * @param path {String} 变量路径
 */
function collectStubPointer(variable, result, testCase, path = '') {
  const currentPath = path === '' ? variable.name : [path, variable.name].join('.');
  collectStubData(variable, result, path, currentPath, collectStubPointer, testCase);
}

/**
 * 收集桩函数子数据
 * @param stubs {Object} 桩函数
 * @param stubName {String} 桩函数名
 * @param field {String} 桩函数域
 * @param sign {String} 函数签名
 */
function collectStubsChildren(stubs, stubName, field, functionYml, testCase, sign) {
  let currentTime = 0;
  const pointTargets = {};
  const testCaseTimes = [];
  stubs.children.forEach((stub) => {
    const oneTime = {};
    while (stub.time > currentTime) {
      try {
        functionYml[field][stubName].tests.push({ times: oneTime });
      } catch (err) {
        logger.error('', err);
      }
      currentTime += 1;
    }
    stub.children.forEach((variable) => {
      if (variable.kind === testCase.VARIABLE_KIND.stubParam
        && variable.type === testCase.VARIABLE_TYPE.pointer
        && variable.data['@value']
        && variable.data['@value'] !== '0'
      ) {
        pointTargets[variable.data['@value'].replace('@', '')] = variable.name;
      }
      switch (variable.kind) {
        case testCase.VARIABLE_KIND.stubReturn:
          collectStubReturn(variable, oneTime, testCase);
          break;
        case testCase.VARIABLE_KIND.stubPointerTarget:
          collectStubPointer(variable, oneTime, testCase);
          break;
        case testCase.VARIABLE_KIND.stubParam:
          collectStubParams(variable, oneTime, testCase);
          break;
        default:
          break;
      }
    });
    Object.keys(oneTime).forEach((variableName) => {
      let name = variableName.split('.')[0];
      if (name.endsWith(']')) {
        [name] = variableName.split('[');
      }
      if (pointTargets[name]) {
        const newName = variableName.replace(name, pointTargets[name]);
        oneTime[newName] = oneTime[variableName];
        delete oneTime[variableName];
      }
    });
    Object.keys(oneTime).filter((name) => oneTime[name]).forEach((variableName) => {
      let value = oneTime[variableName].split('.')[0];
      if (value.endsWith(']')) {
        [value] = value.split('[');
      }
      if (value && pointTargets[value]) {
        oneTime[variableName] = oneTime[variableName].replace(value, pointTargets[value]);
      }
    });
    try {
      testCaseTimes.push(oneTime);
    } catch (err) {
      logger.error('', err);
    }
    currentTime += 1;
  });
  try {
    const data = { times: testCaseTimes };
    if (sign) {
      data.sign = sign;
    }
    functionYml[field][stubName].tests.push(data);
  } catch (err) {
    logger.error('', err);
  }
}

/**
 * 生成yml文件
 * @param {Array} testCases 同一函数所有测试用例
 * @param {String} versionId 版本Id
 * @param {Object} functionVariable 函数变量
 * @param {Object} typeSystem 类型系统
 * @param {ParsedPath} filePath 文件路径
 * @param {String} functionName 函数名称
 * @param {Object} locale 内含国际化配置
 * @returns {Promise}
 */
function generateYmlFile({
  testCases,
  folderPath,
  functionVariable,
  typeSystem,
  filePath,
  functionName,
  locale,
}) {
  let functionYml;
  let ymlPath;
  let templatePath;
  const mallocDeclaration = [];
  return new Promise((resolve, reject) => {
    functionYml = {
      version: YML_VERSION,
      date: moment().format('YYYY-MM-DD'),
      stubs: {},
      originStubs: {},
      variables: [],
      variableTypes: [],
      values: [],
      mallocDeclaration: [],
    };
    const functionValueMessage = {
      input: {
        globalVariablesData: {},
        paramVariablesData: {},
        mallocVariablesData: {},
      },
      output: {
        globalOutputData: {},
        returnValueData: {},
        mallocOutputData: {},
      },
    };
    testCases.forEach((testCase, index) => {
      testCase = new YMLTestCaseTransformer(
        tests.getJson(testCase, locale),
        functionVariable,
        typeSystem,
        // eslint-disable-next-line no-underscore-dangle
        testCase._id,
        locale
      );
      testCase.transform();
      const stubsUsed = new Set();

      ['globalVariablesData', 'paramVariablesData'].forEach((area) => {
        testCase[area].forEach((variable) => {
          collectVariableName(area, 'input', variable, testCase, functionValueMessage, stubsUsed, index, '');
        });
      });
      ['mallocVariablesData'].forEach((area) => {
        testCase[area].forEach((variable) => {
          let declaration;
          if (variable.type === testCase.VARIABLE_TYPE.array) {
            declaration = `${variable.elementTypeName} ${variable.name}[${variable.length}]`;
          } else {
            declaration = `${variable.typeName} ${variable.name}`;
          }
          mallocDeclaration.push(declaration);
          collectVariableName(area, 'input', variable, testCase, functionValueMessage, stubsUsed, index, '');
        });
      });
      ['globalOutputData', 'mallocOutputData'].forEach((area) => {
        testCase[area].forEach((variable) => {
          collectVariableName(area, 'output', variable, testCase, functionValueMessage, stubsUsed, index, '');
        });
      });
      ['returnValueData'].forEach((area) => {
        testCase[area].forEach((variable) => {
          collectVariableName(area, 'output', variable, testCase, functionValueMessage, stubsUsed, index, `${functionName}()`);
        });
      });

      const stubWithId = {};
      testCase.stubsData.forEach((stubs) => {
        if (!stubsUsed.has(stubs.stubName)) {
          const { stubName } = stubs;
          if (!functionYml.originStubs[stubName]) {
            functionYml.originStubs[stubName] = {
              returnName: '_ret',
              returnType: stubs.typeName,
              tests: [],
            };
          }
          collectStubsChildren(stubs, stubName, 'originStubs', functionYml, testCase);
        } else {
          const { stubName, typeName, name } = stubs;
          const sign = `${typeName} ${name.replace(stubName, stubName + testCase.id)}`;
          stubWithId[stubName] = stubName + testCase.id;
          if (!functionYml.stubs[stubName]) {
            functionYml.stubs[stubName] = {
              returnName: '_ret',
              returnType: stubs.typeName,
              tests: [],
            };
          }
          collectStubsChildren(stubs, stubName, 'stubs', functionYml, testCase, sign);
        }
      });
    });

    const { input, output } = functionValueMessage;
    const value = [];
    for (let i = 0; i < testCases.length; i += 1) {
      value.push([]);
    }
    let areas = Object.keys(input);
    areas.forEach((area) => {
      const variableNames = Object.keys(input[area]);
      variableNames.forEach((variableName) => {
        functionYml.variables.push(variableName);
        functionYml.variableTypes.push('i');
        const values = input[area][variableName].data;
        values.forEach((val, index) => {
          value[index].push(val);
        });
      });
    });
    areas = Object.keys(output);
    areas.forEach((area) => {
      const names = Object.keys(output[area]);
      names.forEach((variableName) => {
        functionYml.variables.push(variableName);
        functionYml.variableTypes.push('o');
        output[area][variableName].data.forEach((data, index) => {
          value[index].push(data);
        });
      });
    });
    functionYml.values = value;
    functionYml.mallocDeclaration = Array.from(new Set(mallocDeclaration));
    templatePath = path.join(__dirname, '/template', 'tests.yml.dot');
    filePath = path.parse(filePath);
    const [fileName] = filePath.base.split(':');
    filePath.dir = path.join(folderPath, filePath.dir, fileName);
    filePath.ext = '.yml';
    filePath.base = functionName + filePath.ext;
    ymlPath = path.format(filePath);
    fs.ensureDir(filePath.dir)
      .then(() => utility.getDotTemplate(templatePath, functionYml))
      .then((ymlContent) => utility.writeFile(ymlContent, ymlPath))
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 生成项目各文件下每个函数的测试用例yml数据，并打包成zip
 * @param versionId {String} 版本Id
 * @param locale {Object} 内含国际化配置
 * @returns {Promise}
 */
function exportTestCasesYmlZip(versionId, folderPath, locale) {
  return new Promise((resolve, reject) => {
    const typeSystemMap = {};
    const functionMap = {};
    const fileMap = {};
    const functionVariableMap = {};
    const testGroups = {};
    Promise.all([functions.getFunctions(versionId), files.getFiles(versionId)])
      .then(([funcs, files]) => {
        funcs.forEach((func) => {
          // eslint-disable-next-line no-underscore-dangle
          const functionId = func._id;
          functionMap[functionId] = func;
        });
        files.forEach((file) => {
          // eslint-disable-next-line no-underscore-dangle
          const fileId = file._id;
          fileMap[fileId] = file;
        });
      })
      .then(() => FunctionVariable.find({ versionId }))
      .then((functionVariables) => {
        functionVariables.forEach((functionVariable) => {
          functionVariable = functions.getFunctionVariableJson(functionVariable);
          functionVariableMap[functionVariable.functionId] = functionVariable;
        });
      })
      .then(() => files.getFileTypeSystemByVersion(versionId))
      .then((fileTypeSystems) => {
        fileTypeSystems.forEach((fileTypeSystem) => {
          typeSystemMap[fileTypeSystem.fileId] = fileTypeSystem;
        });
      })
      .then(() => TestCase.find({ versionId }))
      .then((testCases) => {
        testCases.forEach((testCase) => {
          if (!testGroups[testCase.functionId]) {
            testGroups[testCase.functionId] = [];
          }
          testGroups[testCase.functionId].push(testCase);
        });
      })
      .then(() => {
        const promises = [];
        Object.keys(functionMap).forEach((functionId) => {
          const { fileId, functionName } = functionMap[functionId];
          const filePath = fileMap[fileId].path;
          const functionVariable = functionVariableMap[functionId];
          const typeSystem = files.getTypesJson(fileId, typeSystemMap[fileId]);
          if (testGroups[functionId]) {
            promises.push(generateYmlFile({
              testCases: testGroups[functionId],
              folderPath,
              functionVariable,
              typeSystem,
              filePath,
              functionName,
              locale,
            }));
          }
        });
        return Promise.all(promises);
      })
      .then(resolve)
      .catch(reject);
  });
}

module.exports = {
  exportTestCasesYmlZip,
};
